β‘ Plotly#
This tutorial shows how to build interactive visualizations for energy data using Plotly Express. β¨ Unlike static Matplotlib/Seaborn plots, Plotly charts let you:
π Zoom, pan, and reset views
π±οΈ Hover to see tooltips
β Toggle lines and categories in the legend
πΎ Export to HTML or PNG
Setup & Imports π οΈ#
# If needed, install Plotly:
# !pip install plotly
import numpy as np
import pandas as pd
import plotly.express as px
from IPython.display import HTML, display
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Use a clean white theme
px.defaults.template = "plotly_white"
Interactive Line: Hourly Demand β°#
Line plots are the bread and butter of energy time series. With Plotly, you can zoom into hours, hover over values, and explore patterns interactively.
rng = pd.date_range("2024-01-01", periods=24*7, freq="h")
demand = 1200 + 250*np.sin(2*np.pi*(rng.hour/24)) + np.random.normal(0, 40, len(rng))
df = pd.DataFrame({"time": rng, "demand_MW": demand})
fig = px.line(df, x="time", y="demand_MW",
title="Hourly Electricity Demand (Interactive)")
fig.update_layout(xaxis_title="Time", yaxis_title="Demand [MW]")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()
Comparing Weeks π #
Overlay multiple weeks and use the legend to toggle lines on/off.
rng = pd.date_range("2024-02-01", periods=24*7, freq="h")
week1 = 1100 + 240*np.sin(2*np.pi*(rng.hour/24)) + np.random.normal(0, 35, len(rng))
week2 = week1 * 1.07 + np.random.normal(0, 20, len(rng)) # ~7% higher
df2 = pd.DataFrame({
"time": list(rng) * 2,
"demand_MW": np.r_[week1, week2],
"week": ["Week 1"] * len(rng) + ["Week 2"] * len(rng)
})
fig = px.line(df2, x="time", y="demand_MW", color="week",
title="Two Weeks of Demand")
fig.update_layout(xaxis_title="Time", yaxis_title="Demand [MW]")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()
Range Slider & Buttons β³#
Add interactive time filters β switch between 1 day, 3 days, 1 week, or full range.
fig = px.line(df, x="time", y="demand_MW", title="Demand with Range Slider")
fig.update_xaxes(rangeslider_visible=True,
rangeselector=dict(
buttons=list([
dict(count=24, label="1D", step="hour", stepmode="backward"),
dict(count=3, label="3D", step="day", stepmode="backward"),
dict(count=7, label="1W", step="day", stepmode="backward"),
dict(step="all")
])
))
fig.update_layout(xaxis_title="Time", yaxis_title="Demand [MW]")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()
Stacked Area: Generation Mix ππ¨π§#
Stacked areas are great for showing supply mix (Solar, Wind, Hydro).
days = pd.date_range("2024-03-01", periods=14, freq="D")
solar = np.clip(200 + 50*np.sin(2*np.pi*(days.dayofyear/365)), 150, 300)
wind = 300 + 80*np.sin(2*np.pi*(days.dayofyear/14) + 1)
hydro = 500 + np.random.normal(0, 25, len(days))
mix = pd.DataFrame({"day": days, "Solar": solar, "Wind": wind, "Hydro": hydro})
long = mix.melt(id_vars="day", var_name="source", value_name="MWh")
fig = px.area(long, x="day", y="MWh", color="source",
title="Daily Generation Mix (Stacked Area)")
fig.update_layout(xaxis_title="Day", yaxis_title="Energy [MWh]")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()
Scatter with Color & Size π―#
Scatterplots let us show demand vs temperature, with season as color and wind as bubble size.
np.random.seed(0)
n = 500
temp = np.random.uniform(-15, 30, n)
demand2 = 1250 + 5*(temp-15)**2 + np.random.normal(0, 60, n)
wind = np.random.uniform(0, 20, n)
season = pd.cut(temp, bins=[-50,0,15,50], labels=["Winter","Spring/Autumn","Summer"])
df_sc = pd.DataFrame({"temp": temp, "demand": demand2, "wind": wind, "season": season})
fig = px.scatter(df_sc, x="temp", y="demand",
color="season", size="wind",
labels={"temp":"Temperature (Β°C)", "demand":"Demand [MW]", "wind":"Wind speed (m/s)"},
title="Demand vs Temperature (Color=Season, Size=Wind)")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()
Correlation Heatmap π₯#
See which variables move together using a correlation heatmap.
weather = pd.DataFrame({
"demand": [2000, 2100, 2200, 1900, 1800, 1700, 1600],
"temp": [-5, 0, 5, 10, 15, 20, 25],
"wind": [2, 3, 4, 5, 6, 4, 3],
"humidity":[85, 80, 75, 70, 65, 60, 55],
"solar": [50, 100, 200, 400, 600, 750, 850]
})
corr = weather.corr(numeric_only=True)
fig = px.imshow(corr, text_auto=".2f", color_continuous_scale="RdBu_r",
title="Correlation Heatmap (Demand & Weather)")
fig.update_xaxes(side="bottom")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()
Facets (Small Multiples) πΌοΈ#
Break down scatterplots by weekday to see differences.
df_sc["weekday"] = np.random.choice(
["Mon","Tue","Wed","Thu","Fri","Sat","Sun"], size=len(df_sc)
)
fig = px.scatter(df_sc, x="temp", y="demand", color="season",
facet_col="weekday", facet_col_wrap=4, opacity=0.6,
title="Demand vs Temperature by Weekday")
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()
Bar Charts π#
Compare categories (e.g., regions).
regions = ["North","South","East","West"]
demand_r = [1400, 1600, 1200, 1100]
df_bar = pd.DataFrame({"Region": regions, "Demand": demand_r})
fig = px.bar(df_bar, x="Region", y="Demand", text="Demand",
title=" Regional Electricity Demand")
fig.update_traces(textposition="outside")
fig.update_layout(yaxis_title="Demand [MWh]")
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()
Dual-Axis Chart ππΆ#
Sometimes we need two scales β e.g., demand [MW] vs price [β¬/MWh].
from plotly.subplots import make_subplots
import plotly.graph_objects as go
# Dates
idx = pd.date_range("2024-04-01", periods=30, freq="D")
# Generate demand as a NumPy array
demand_d = 1300 + 150*np.sin(2*np.pi*(np.arange(len(idx))/7)) + np.random.normal(0, 40, len(idx))
# Now demand_d is an array, so .mean() works
price = 50 + 0.07*(demand_d - demand_d.mean()) + np.random.normal(0, 4, len(idx))
# Dual-axis plot
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(go.Scatter(x=idx, y=demand_d, name="Demand [MW]", mode="lines+markers"), secondary_y=False)
fig.add_trace(go.Bar(x=idx, y=price, name="Price [β¬/MWh]", opacity=0.5), secondary_y=True)
fig.update_layout(title_text="Dual-Axis: Demand vs Price",
bargap=0.2, legend=dict(orientation="h", y=1.1))
fig.update_xaxes(title_text="Day")
fig.update_yaxes(title_text="Demand [MW]", secondary_y=False)
fig.update_yaxes(title_text="Price [β¬/MWh]", secondary_y=True)
display(HTML(fig.to_html(include_plotlyjs=True, full_html=False)))
#fig.show()
Wrap-up β¨#
Use line/area plots for time series.
Use scatter/bubbles for relationships.
Use heatmaps/facets for multivariate comparisons.
Use dual-axis when two scales matter (but donβt overuse it).
π More Resources#
For more details and advanced usage of Plotly in Python, check out the official documentation: